package com.davfx.httpnio;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CoderResult;
import java.util.Iterator;
import java.util.Map;

public final class HttpClient {

	private static final int CHAR_BUFFER_SIZE = 10 * 1024;
	
	private final HttpConnecter connecter;
	
	public HttpClient(Worker worker) {
		connecter = new HttpConnecter(worker);
	}
	
	public static interface HttpResponseHandler {
		void handle(HttpResponse response);
		void closed();
		void failed(IOException e);
	}
	public static interface TextHttpResponseHandler extends HttpResponseHandler {
		void handle(String content);
	}
	public static interface PageHttpResponseHandler {
		void handle(HttpResponse response, String page);
		void failed(IOException e);
	}
	public static interface RawHttpResponseHandler extends HttpResponseHandler {
		void handle(ByteBuffer content);
	}
	public static interface Connection {
		void write(ByteBuffer buffer);
		void close();
	}
	
	public Connection send(HttpRequest request, final RawHttpResponseHandler handler) {
		ByteBuffer toPost = prepareHeaders(request);
		HttpConnection connection = connecter.connect(request.getHost(), request.getPort(), request.getMethod().toString(), getPath(request), request.getHeaders(), new HttpReadConnectHandler() {
			@Override
			public void handle(ByteBuffer buffer) {
				handler.handle(buffer);
			}
			@Override
			public void handle(int statusCode, String reason, Map<String, String> headers) {
				handler.handle(new HttpResponse(statusCode, reason, headers));
			}
			@Override
			public void failed(IOException e) {
				handler.failed(e);
			}
			@Override
			public void closed() {
				handler.closed();
			}
		});
		return send(request, connection, toPost, handler);
	}
	
	private static Connection send(HttpRequest request, final HttpConnection c, ByteBuffer toPost, final HttpResponseHandler handler) {
		if (toPost != null) {
			return new Connection() {
				@Override
				public void close() {
					c.close();
				}
				@Override
				public void write(ByteBuffer buffer) {
					handler.failed(new IOException("Cannot write after POST"));
				}
			};
		} else {
			String l = request.getHeaders().get(Http.CONTENT_LENGTH);
			long contentLength = -1;
			if (l != null) {
				try {
					contentLength = Long.parseLong(l);
				} catch (NumberFormatException e) {
				}
			}
			if (contentLength < 0) {
				return new Connection() {
					@Override
					public void close() {
						c.close();
					}
					@Override
					public void write(ByteBuffer buffer) {
						handler.failed(new IOException("Cannot write, content length missing"));
					}
				};
			} else {
				return new Connection() {
					@Override
					public void close() {
						c.close();
					}
					@Override
					public void write(ByteBuffer buffer) {
						c.write(buffer);
					}
				};
			}
		}
	}
	
	private static String buildParametersText(HttpRequest request) {
		StringBuilder b = new StringBuilder();
		for (Iterator<Map.Entry<String, String>> i = request.getParameters().entrySet().iterator(); i.hasNext();) {
			Map.Entry<String, String> e = i.next();
			b.append(Http.Url.encode(e.getKey()));
			b.append(Http.PARAMETER_KEY_VALUE_SEPARATOR);
			b.append(Http.Url.encode(e.getValue()));
			if (i.hasNext()) {
				b.append(Http.PARAMETERS_SEPARATOR);
			}
		}
		return b.toString();
	}
	
	private static String getPath(HttpRequest request) {
		if (request.getMethod() != HttpRequest.Method.POST) {
			StringBuilder b = new StringBuilder(request.getPath());
			b.append(Http.PARAMETERS_START).append(buildParametersText(request));
			return b.toString();
		} else {
			return request.getPath();
		}
	}
	
	private static ByteBuffer prepareHeaders(HttpRequest request) {
		Map<String, String> headers = request.getHeaders();
		if (request.getMethod() == HttpRequest.Method.POST) {
			ByteBuffer toPost = ByteBuffer.wrap(buildParametersText(request).getBytes(Http.USASCII_CHARSET));
			headers.put(Http.CONTENT_LENGTH, String.valueOf(toPost.remaining()));
			return toPost;
		} else {
			return null;
		}
	}
	
	public Connection send(HttpRequest request, TextHttpResponseHandler handler) {
		ByteBuffer toPost = prepareHeaders(request);
		HttpConnection connection = connecter.connect(request.getHost(), request.getPort(), request.getMethod().toString(), getPath(request), request.getHeaders(), new InnerTextReadHandler(handler));
		return send(request, connection, toPost, handler);
	}
	
	public Connection send(HttpRequest request, final PageHttpResponseHandler handler) {
		return send(request, new TextHttpResponseHandler() {
			private HttpResponse response;
			private final StringBuilder b = new StringBuilder();
			@Override
			public void handle(HttpResponse response) {
				this.response = response;
			}
			@Override
			public void failed(IOException e) {
				handler.failed(e);
			}
			@Override
			public void closed() {
				handler.handle(response, b.toString());
			}
			@Override
			public void handle(String content) {
				b.append(content);
			}
		});
	}
	
	private static final class InnerTextReadHandler implements HttpReadConnectHandler {
		private final TextHttpResponseHandler handler;
		private CharsetDecoder decoder;
		private final CharBuffer out = CharBuffer.allocate(CHAR_BUFFER_SIZE);
		
		public InnerTextReadHandler(TextHttpResponseHandler handler) {
			this.handler = handler;
		}
		
		@Override
		public void handle(int statusCode, String reason, Map<String, String> headers) {
			String contentType = headers.get(Http.CONTENT_TYPE);
			Charset charset = null;
			if (contentType != null) {
				int i = contentType.indexOf(Http.EXTENSION_SEPARATOR);
				if (i >= 0) {
					contentType = contentType.substring(i + 1).trim();
					int j = contentType.indexOf(Http.HEADER_KEY_VALUE_SEPARATOR);
					if (j >= 0) {
						if (Http.CHARSET.equalsIgnoreCase(contentType.substring(0, j))) {
							try {
								charset = Charset.forName(contentType.substring(j + 1));
							} catch (Exception e) {
							}
						}
					}
				}
			}
			if (charset == null) {
				charset = Http.DEFAULT_CHARSET;
			}
			decoder = charset.newDecoder();
			handler.handle(new HttpResponse(statusCode, reason, headers));
		}
		
		@Override
		public void handle(ByteBuffer content) {
			while (true) {
				CoderResult result = decoder.decode(content, out, false);
				out.flip();
				handler.handle(out.toString());
				out.rewind();
				if (result == CoderResult.UNDERFLOW) {
					break;
				}
			}
		}
		
		@Override
		public void closed() {
			handler.closed();
		}
		
		@Override
		public void failed(IOException e) {
			handler.failed(e);
		}
	}
}
